class GlobalRulesConfig {

  groupRules = [];
  isGlobalOpen = false;

  constructor(options = { groupRules: [], isGlobalOpen: false }) {
    this.groupRules = options.groupRules;
    this.isGlobalOpen = options.isGlobalOpen;
  }

  setGroupRules(groupRules = []) {
    this.groupRules = groupRules;
  }

  getGroupRules() {
    return this.groupRules || [];
  }

  setIsGlobalOpen(isGlobalOpen = false) {
    this.isGlobalOpen = isGlobalOpen;
  }

  getIsGlobalOpen() {
    return this.isGlobalOpen;
  }

  getMathRequest(matchOptions) {
    if (!matchOptions) {
      return;
    }
    const groupRules = this.getGroupRules();
    const length = groupRules.length;
    let findResult = undefined;
    for (let i = 0; i < length; i++) {
      const currentChildren = groupRules[i].children || [];
      if (!currentChildren.length) {
        continue;
      }
      findResult = currentChildren.find(item => {
        const { isOpen, method, url } = item.request || {};
        return isOpen && matchOptions.method === method && matchOptions.url == url;
      });
      if (findResult) {
        break;
      }
    }
    return findResult && findResult.request;
  }

  getRequestUrl(url, request) {
    const params = request && request.params;
    if (!Array.isArray(params)) {
      return url;
    }
    const result = params.filter(item => item.name && item.isChecked).map(item => {
      return `${item.name}=${item.value}`;
    });
    if (!result.length) {
      return url;
    }
    return [url, result.join('&')].join(url.includes('?') ? '&' : '?');
  }
}

class RequestOption {
  method = 'get';
  body = undefined;
  url = '';
  constructor() { }
}

// keep origin ajax
const OriginAjax = Object.freeze({
  XMLHttpRequest: window.XMLHttpRequest,
  fetch: window.fetch.bind(window),
  Request: window.Request,
});

class LocalRequest extends OriginAjax.Request {

  // override attribute
  _url = '';
  _options;

  constructor(...props) {
    super(...props);
    this._url = props[0];
    this._options = props[1];
  }
}

class LocalXMLHttpRequest extends OriginAjax.XMLHttpRequest {

  options = new RequestOption();

  currentRequest;

  // override attribute
  _responseText = '';
  _response = '';
  _status = undefined;

  constructor() {
    super();
    this.initConfig();
  }

  initConfig() {
    const xhrOpen = this.open;
    this.open = (...openParams) => {
      this.options.method = openParams[0].toUpperCase();
      let url = openParams[1];
      if (!url.startsWith('http') && !url.startsWith('data:')) {
        url = location.origin + url;
      }
      this.options.url = url;

      this.currentRequest = globalRulesConfig.getMathRequest(this.options);
      openParams[1] = globalRulesConfig.getRequestUrl(openParams[1], this.currentRequest);
      xhrOpen.apply(this, openParams);
      // set request headers
      this.setRequestHeaders();

    }
    const xhrSend = this.send;
    this.send = (sendParams) => {
      // reset body
      this.monitorChange();
      this.options.body = sendParams;
      xhrSend.call(this, this.currentRequest && this.currentRequest.body || sendParams);
    }
  }

  monitorChange() {
    this.onreadystatechange = () => {
      if (this.readyState == 4) {
        if (globalRulesConfig.getIsGlobalOpen()) {
          this.modifyResponse();
        }
      }
    }
  }

  overrideAttr() {
    ['responseText', 'response', 'status'].forEach(attr => {
      Object.defineProperty(this, attr, {
        get: () => this[`_${attr}`],
        set: (val) => this[`_${attr}`] = val,
        enumerable: true
      });
    });
  }

  modifyResponse() {
    if (this.currentRequest) {
      this.overrideAttr();
      const { resBody, resStatus } = this.currentRequest;
      this.responseText = resBody;
      this.response = resBody;
      if (typeof resStatus === 'number') {
        this.status = resStatus;
      }
    }
  }

  setRequestHeaders() {
    const headers = this.currentRequest && this.currentRequest.headers;
    if (!Array.isArray(headers)) {
      return;
    }
    headers.forEach(item => {
      if (item.isChecked && item.name) {
        this.setRequestHeader(item.name, item.value);
      }
    });
  }
}


class LocalXHR {

  options = new RequestOption();
  currentRequest;
  xhr = new OriginAjax.XMLHttpRequest;

  constructor() {
    this.initAttr();
  }

  open = (...openParams) => {
    this.options.method = openParams[0].toUpperCase();
    let url = openParams[1];
    if (!url.startsWith('http') && !url.startsWith('data:')) {
      url = location.origin + url;
    }
    this.options.url = url;
    this.currentRequest = globalRulesConfig.getMathRequest(this.options);
    if (this.currentRequest && typeof this.currentRequest.timeout === 'number') {
      this.xhr.timeout = this.currentRequest.timeout;
    }
    openParams[1] = globalRulesConfig.getRequestUrl(openParams[1], this.currentRequest);

    this.xhr.open(...openParams);
    // set request headers
    this.setRequestHeaders();
  }

  send = (sendParams) => {
    this.options.body = sendParams;
    this.xhr.send(this.currentRequest && this.currentRequest.body || sendParams);
  };

  initAttr = () => {
    const xhr = this.xhr;
    for (let attr in xhr) {
      if (attr === 'onreadystatechange') {
        xhr.onreadystatechange = (...args) => {
          if (this.readyState == 4) {
            this.modifyResponse();
          }
          this.onreadystatechange && this.onreadystatechange.apply(this, args);
        };
        continue;
      }
      if (typeof xhr[attr] === 'function') {
        if (!['send', 'open'].includes(attr)) {
          this[attr] = xhr[attr].bind(xhr);
        }
      } else {
        if (['responseText', 'response', 'status'].includes(attr)) {
          Object.defineProperty(this, attr, {
            get: () => this[`_${attr}`] == undefined ? xhr[attr] : this[`_${attr}`],
            set: (val) => this[`_${attr}`] = val,
            enumerable: true
          });
        } else {
          Object.defineProperty(this, attr, {
            get: () => xhr[attr],
            set: (val) => xhr[attr] = val,
            enumerable: true
          });
        }
      }
    }
  }

  modifyResponse = () => {
    if (!globalRulesConfig.getIsGlobalOpen() || !this.currentRequest) {
      return;
    }
    const { resBody, resStatus } = this.currentRequest;
    this.responseText = resBody;
    this.response = resBody;
    if (typeof resStatus === 'number') {
      this.status = resStatus;
    }
  }

  setRequestHeaders = () => {
    const xhr = this.xhr;
    const headers = this.currentRequest && this.currentRequest.headers;
    if (!Array.isArray(headers) || !xhr) {
      return;
    }
    headers.forEach(item => {
      if (item.isChecked && item.name) {
        xhr.setRequestHeader(item.name, item.value);
      }
    });
  };
}


const LocalFetch = {
  fetch: (...args) => {
    const options = LocalFetch.getOptions(args);
    const currentRequest = globalRulesConfig.getMathRequest(options);
    const resetArgs = LocalFetch.resetOptions(args, currentRequest);
    return OriginAjax.fetch(...resetArgs).then((response) => {
      if (!currentRequest || currentRequest.resBody === undefined) {
        return response;
      }
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode(currentRequest.resBody));
          controller.close();
        }
      });

      const newResponse = new Response(stream, {
        status: typeof currentRequest.resStatus === 'number' ? currentRequest.resStatus : response.status,
      });
      const proxy = new Proxy(newResponse, {
        get: function (target, name) {
          return ['body', 'bodyUsed', 'headers',
            'ok', 'redirected', 'statusText',
            'type', 'url'].includes(name) ? response[name] : target[name];
        }
      });
      for (let key in proxy) {
        if (typeof proxy[key] === 'function') {
          proxy[key] = proxy[key].bind(newResponse);
        }
      }
      return proxy;
    });
  },

  /**
   * return {url ,body, method}
   */
  getOptions: (collection = []) => {
    let method, body, url;
    const reqCollection = collection[0];
    if (Object.prototype.toString.call(reqCollection) === '[object Request]') {
      method = reqCollection.method;
      body = reqCollection.body;
      url = reqCollection.url;
    } else {
      url = reqCollection;
      if (collection[1]) {
        method = collection[1].method;
        body = collection[1].body;
      }
    }
    if (!url.startsWith('http') && !url.startsWith('data:')) {
      url = location.origin + url;
    }
    method = (method || 'GET').toUpperCase();
    return { method, url, body };
  },

  resetOptions: (collection = [], request) => {
    if (!request) {
      return collection;
    }
    let url, options = undefined;
    if (Object.prototype.toString.call(collection[0]) === '[object Request]') {
      url = collection[0].url;
      options = collection[0]._options || {};
    } else {
      url = collection[0];
      options = collection[1] || {};
    }
    url = globalRulesConfig.getRequestUrl(url, request);
    const method = (options.method || 'GET').toUpperCase();

    const headers = {};
    if (Array.isArray(request.headers)) {
      request.headers.forEach(item => {
        if (item.isChecked && item.name) {
          headers[item.name] = item.value;
        }
      });
    }

    let signal = undefined;
    if (typeof request.timeout === 'number') {
      signal = AbortSignal.timeout(request.timeout);
    }

    const myOptions = {
      ...options,
      method,
      signal,
      body: method !== 'GET' && (request.body || options.body) || undefined,
      headers: Object.assign(options.headers || {}, headers),
    };
    const myRequest = new Request(url, myOptions);
    return [myRequest];
  }
};


const globalRulesConfig = new GlobalRulesConfig();

// detail {type: string, data: any}
window.addEventListener("globalRulesConfigMsg", function (event) {
  if (!globalRulesConfig) {
    globalRulesConfig = new globalRulesConfig();
  }
  const msgDetail = event.detail;
  if (!msgDetail || msgDetail.type !== 'updateConfig') {
    return;
  }
  const configData = msgDetail.data || {};
  if (typeof configData.isGlobalOpen === 'boolean') {
    globalRulesConfig.setIsGlobalOpen(configData.isGlobalOpen);
  }
  if (Array.isArray(configData.groupRules)) {
    globalRulesConfig.setGroupRules(configData.groupRules);
  }
  if (globalRulesConfig.getIsGlobalOpen()) {
    // window.XMLHttpRequest = LocalXMLHttpRequest;
    window.XMLHttpRequest = LocalXHR;
    window.fetch = LocalFetch.fetch;
    window.Request = LocalRequest;
  } else {
    window.XMLHttpRequest = OriginAjax.XMLHttpRequest;
    window.fetch = OriginAjax.fetch;
    window.Request = OriginAjax.Request;
  }
}, false);